package com.huawei.admins.platform.gateway.core.component.router.locator;

import com.huawei.admins.platform.gateway.core.component.filter.FilterDefinition;
import com.huawei.admins.platform.gateway.core.component.filter.GatewayFilter;
import com.huawei.admins.platform.gateway.core.component.filter.GatewayFilterFactory;
import com.huawei.admins.platform.gateway.core.component.predicate.AsyncPredicate;
import com.huawei.admins.platform.gateway.core.component.predicate.PredicateDefinition;
import com.huawei.admins.platform.gateway.core.component.predicate.factory.RoutePredicateFactory;
import com.huawei.admins.platform.gateway.core.component.router.RouteDefinition;
import com.huawei.admins.platform.gateway.core.component.router.Router;
import com.huawei.admins.platform.gateway.core.component.router.RouterLocator;
import com.huawei.admins.platform.gateway.core.config.GatewayProperties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.util.CollectionUtils;
import org.springframework.web.server.ServerWebExchange;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public abstract class AbstractRouteDefinitionRouteLocator implements RouterLocator {

    protected final Log logger = LogFactory.getLog(getClass());

    private final GatewayProperties gatewayProperties;

    private final Map<String, RoutePredicateFactory> predicates = new LinkedHashMap<>();

    private final Map<String, GatewayFilterFactory> gatewayFilterFactories = new HashMap<>();


    public AbstractRouteDefinitionRouteLocator(GatewayProperties gatewayProperties, List<RoutePredicateFactory> predicates, List<GatewayFilterFactory> filterFactories) {
        this.gatewayProperties = gatewayProperties;
        initFactories(predicates);
        filterFactories.forEach(factory -> this.gatewayFilterFactories.put(factory.name(), factory));
    }

    private void initFactories(List<RoutePredicateFactory> predicates) {
        predicates.forEach(factory -> {
            String key = factory.name();
            if (this.predicates.containsKey(key)) {
                this.logger.warn("A RoutePredicateFactory named " + key + " already exists, class: "
                        + this.predicates.get(key) + ". It will be overwritten.");
            }
            this.predicates.put(key, factory);
            if (logger.isInfoEnabled()) {
                logger.info("Loaded RoutePredicateFactory [" + key + "]");
            }
        });
    }

    protected final Router convertToRoute(RouteDefinition routeDefinition) {
        AsyncPredicate<ServerWebExchange> predicate = combinePredicates(routeDefinition);
        List<GatewayFilter> gatewayFilters = getFilters(routeDefinition);

        return Router.async(routeDefinition).asyncPredicate(predicate).replaceFilters(gatewayFilters).build();
    }

    private List<GatewayFilter> getFilters(RouteDefinition routeDefinition) {

        List<GatewayFilter> filters = new ArrayList<>();
        if (!this.gatewayProperties.getDefaultFilters().isEmpty()) {
            filters.addAll(loadGatewayFilters(routeDefinition.getId(),
                    new ArrayList<>(this.gatewayProperties.getDefaultFilters())));
        }
        List<FilterDefinition> definitionFilters = routeDefinition.getFilters();
        if (!CollectionUtils.isEmpty(definitionFilters)) {
            filters.addAll(loadGatewayFilters(routeDefinition.getId(), definitionFilters));
        }
        AnnotationAwareOrderComparator.sort(filters);
        return filters;
    }

    List<GatewayFilter> loadGatewayFilters(String id, List<FilterDefinition> filterDefinitions) {
        ArrayList<GatewayFilter> ordered = new ArrayList<>(filterDefinitions.size());
        for (int i = 0; i < filterDefinitions.size(); i++) {
            FilterDefinition definition = filterDefinitions.get(i);
            GatewayFilterFactory factory = this.gatewayFilterFactories.get(definition.getName());
            if (factory == null) {
                throw new IllegalArgumentException(
                        "Unable to find GatewayFilterFactory with name " + definition.getName());
            }
            if (logger.isDebugEnabled()) {
                logger.debug("RouteDefinition " + id + " applying filter " + definition.getArgs() + " to "
                        + definition.getName());
            }

            GatewayFilter gatewayFilter = factory.apply(id, definition);
            if (gatewayFilter != null) {
                ordered.add(gatewayFilter);
            }
        }

        return ordered;
    }

    /**
     *
     * @param routeDefinition 路由定义对象
     * @return 路由选择规则
     */
    private AsyncPredicate<ServerWebExchange> combinePredicates(RouteDefinition routeDefinition) {
        List<PredicateDefinition> predicates = routeDefinition.getPredicates();
        if (predicates == null || predicates.isEmpty()) {
            // this is a very rare case, but possible, just match all
            return AsyncPredicate.from(exchange -> true);
        }
        AsyncPredicate<ServerWebExchange> predicate = lookup(routeDefinition, predicates.getFirst());

        for (PredicateDefinition andPredicate : predicates.subList(1, predicates.size())) {
            AsyncPredicate<ServerWebExchange> found = lookup(routeDefinition, andPredicate);
            predicate = predicate.and(found);
        }

        return predicate;
    }

    private AsyncPredicate<ServerWebExchange> lookup(RouteDefinition route, PredicateDefinition predicate) {
        RoutePredicateFactory factory = this.predicates.get(predicate.getName());
        if (factory == null) {
            throw new IllegalArgumentException("Unable to find RoutePredicateFactory with name " + predicate.getName());
        }
        if (logger.isDebugEnabled()) {
            logger.debug("RouteDefinition " + route.getId() + " applying " + predicate.getArgs() + " to "
                    + predicate.getName());
        }
        return factory.applyAsync(predicate, gatewayProperties);
    }
}
